import re;
import time,pathlib,os,sys,argparse;
import CodeUtil

patternItemLst = re.compile(r"\s+?([_a-zA-Z0-9\-]+)?\:{1}\s*ItemList",re.DOTALL); 
patternItemUILst = re.compile(r"\s+?([_a-zA-Z0-9\-]+)?\:{1}\s*ItemUIList",re.DOTALL);
pattern3DModel = re.compile(r"\s+?([_a-zA-Z0-9\-]+)?\:{1}\s*NewPersModel3D",re.DOTALL);
patternSpine = re.compile(r"\s+?([_a-zA-Z0-9\-]+)?\:{1}\s*SpineObj",re.DOTALL);
# patternDisposeFun =  re.compile(r" *?[a-zA-Z]+\s+?dispose\(\)\:.*?\{.*\}",re.DOTALL); #找dispose函数
patternDisposeFun =  re.compile(r" *?[a-zA-Z]+\s+?dispose\(.*?\).*?\{.*\}",re.DOTALL); #找dispose函数,兼容没有写返回值的函数体
patternDisposeFun =  re.compile(r" *?[a-zA-Z]*\s*?[^.a-zA-Z0-9]dispose\(.*?\).*?\{.*\}",re.DOTALL); #找dispose函数,兼容没有写返回值的函数体、函数名前面没有写修饰符
# patternOnHideFun =  re.compile(r" *?[a-zA-Z]+\s+?onHide\(\)\:.*?\{.*\}",re.DOTALL);#找onHide函数
patternOnHideFun =  re.compile(r" *?[a-zA-Z]*\s*?[^.a-zA-Z0-9]onHide\(.*?\).*?\{.*\}",re.DOTALL);#找onHide函数，,兼容没有写返回值的函数体,函数名前面没有写修饰符


SUCCESS = "YES"
FAIL="NO"
IS_DEBUG = False;
LOG_FILE_HEAD = "check_"

LOG_FILE = None
TS_File_Num = 0;
NOT_STANDER_COUNT = 0;

def logToFile(*vals):
    print(*vals,file=LOG_FILE)

def writeToErr(msg):
    sys.stderr.write(msg+"\n");

def counterNotStander():
    global NOT_STANDER_COUNT;
    NOT_STANDER_COUNT += 1

def doNotStanderOutPut(msg):
    logToFile(msg)
    writeToErr(msg)
    counterNotStander()

def outputTimerMsgSet(msgArrA,msgArrB):
    setA = set(msgArrA)
    setB = set(msgArrB)
    setC = setA.intersection(setB) #找出交集
    if not setC or len(setC)==0:
        setC = setA
    for msg in setC:
        doNotStanderOutPut(msg)

#是否不合规范的代码,返回True表示不合规范
def checkIsNotStandard(filePath:str,content:str=None)->bool:
    if not content:
        try:
            with open(filePath,"r",encoding="utf8") as f:
                content = f.read();
        except Exception as e:
            logToFile('checkIsNotStandard Exception filePath=',filePath,"e=",e)
    if CodeUtil.isIgnore(filePath,content):
        # logToFile("文件:",filePath,"可以忽略检查..")
        return False;
    fileName = os.path.split(filePath)[1];
    global TS_File_Num
    TS_File_Num+=1
    #dispose函数检测
    pattern = patternDisposeFun
    disposeFun = pattern.search(content);
    onHideFun = patternOnHideFun.search(content)
    totalVars,itemListVars,model3DVars,spineVars = getVars(content) #找出所有 ItemList
    notOnDispose=False 
    notOnHide = False
    spineOnHide = False
    timerNotOnHide = False
    hideFnStr = "[onHide]函数";
    if disposeFun:
        srcFunStr = CodeUtil.subFunc(disposeFun[0],"dispose")
        notOnDispose = checkFuncInvoke(srcFunStr,fileName,totalVars,content,"[dispose]函数")
    else:
        if len(itemListVars)>0:
            msg = "{0}中存在多个[ItemList]但是没有执行[dispose]".format(fileName)
            doNotStanderOutPut(msg)
            notOnDispose = True

    timerFuncs = CodeUtil.getTimerFuncs(content)
    if onHideFun:
        srcFunStr = CodeUtil.subFunc(onHideFun[0],"onHide")
        notOnHide = checkFuncInvoke(srcFunStr,fileName,model3DVars,content,hideFnStr)
        spineOnHide = checkFuncInvoke(srcFunStr,fileName,spineVars,content,hideFnStr,"stop#paused");
        timerNotOnHide = checkTimer(content,srcFunStr,fileName,timerFuncs) #检查timer是否在onHide执行
    else:
        if len(model3DVars)>0:
            notOnHide = True
            msg = "{0}中的[{1}]没有在[onHide]函数执行[dispose]".format(fileName,",".join(model3DVars))
            doNotStanderOutPut(msg);
        if len(spineVars)>0:
            spineOnHide = True
            msg = "{0}中的[{1}]没有在[onHide]函数执行[stop]或[paused]".format(fileName,",".join(spineVars))
            doNotStanderOutPut(msg)
        if len(timerFuncs)>0:
            timerNotOnHide = True
            msg = "{0}注册的[timer]函数[{1}]没有在onHide移除".format(fileName,",".join(timerFuncs))
            doNotStanderOutPut(msg)

    fail = notOnDispose or notOnHide or spineOnHide or timerNotOnHide
    return fail

def checkTimer(content,srcFunStr,fileName,timerFuncs):
    msgArrA = []
    msgArrB = []
    timerNotOnHide,msgArrA = checkTimerNotClear(srcFunStr,fileName,timerFuncs) #检查timer是否在onHide执行
    if timerNotOnHide:
        hideInvokeFns = CodeUtil.findInvokeFuns(srcFunStr)
        if hideInvokeFns:
            timerNotOnHide,msgArrB=checkTimerInvoke(hideInvokeFns,content,fileName,timerFuncs)
    if timerNotOnHide:
        outputTimerMsgSet(msgArrA,msgArrB)
    return timerNotOnHide

#检测对象的某个函数是否执行
def checkFuncInvoke(srcFunStr:str,fileName:str,totalVars:list,content:str,msgFnStr:str,fieldExcFun:str="dispose")->bool:
    """检测对象的某个函数是否执行

    Args:
        srcFunStr (str): 函数体 字符串
        fileName (str): 当前检测的文件名
        totalVars (list): 当前需要检查的字段变量列表
        content (str): 整个文件内容
        msgFnStr (str): 函数描述信息
        fieldExcFun (str, optional): 要检查的执行的函数名. Defaults to "dispose".

    Returns:
        bool: True表没有执行
    """
    notExcFunc=False
    if srcFunStr:
        notExcFunc,msgArr = checkTargetFunNotExc(fileName,srcFunStr,totalVars,msgFnStr,fieldExcFun);
        setResult = None
        if notExcFunc:
            setResult = set(msgArr)
            disposeInvokeFns = CodeUtil.findInvokeFuns(srcFunStr)
            for funcName in disposeInvokeFns:
                temPat = CodeUtil.getPatByFuncName(funcName)
                tempMacth = temPat.search(content)
                if tempMacth:
                    tempFuncStr = CodeUtil.subFunc(tempMacth[0],funcName)
                    tempBool,subMsgArr = checkTargetFunNotExc(fileName,tempFuncStr,totalVars,msgFnStr,fieldExcFun);
                    setResult.intersection_update(set(subMsgArr)) #找出交集
                    if not tempBool:
                        notExcFunc = tempBool
            if not setResult or len(setResult)==0:
                setResult = set(msgArr)
        if notExcFunc and setResult:
            for msg in setResult:
                doNotStanderOutPut(msg)

    return notExcFunc

def checkTimerInvoke(hideInvokeFns:list,content:str,fileName:str,timerFuncs:list):
    timerNotOnHide = True
    msgArr = []
    for funcName in hideInvokeFns:
        temPat = CodeUtil.getPatByFuncName(funcName)
        tempMacth = temPat.search(content)
        if tempMacth:
            tempFuncStr = CodeUtil.subFunc(tempMacth[0],funcName)
            timerNotOnHide,msgArr = checkTimerNotClear(tempFuncStr,fileName,timerFuncs)
    return timerNotOnHide,msgArr

def checkTimerNotClear(funcStr,fileName,timerFuncs)->bool:
    timerNotOnHide = False
    msgArr = []
    i = 0;
    while timerFuncs and len(timerFuncs)>0:
        fn = timerFuncs[i]
        cealrPat=CodeUtil.getClearFnPat(fn)
        m = cealrPat.search(funcStr);
        if not m:
            timerNotOnHide = True
            msg = "{0}注册的[timer]函数[{1}]没有在onHide移除".format(fileName,fn);
            msgArr.append(msg)
        else:
            timerFuncs.pop(i)
            i-=1
        i+=1
        if i>=len(timerFuncs):
            break
    return timerNotOnHide,msgArr

def getVars(content:str):
    # pattern = re.compile(pt+r"\s+?[a-zA-Z0-9]+?\:{1}\s*ItemList",re.DOTALL); 
    # pattern = re.compile(r"\s+?([_a-zA-Z0-9\-]+)?\:{1}\s*ItemList",re.DOTALL); 
    totalVars = []
    itemListVars = []
    itemListVars = patternItemLst.findall(content)
    itemListVars.extend(patternItemUILst.findall(content))
    totalVars.extend(itemListVars)
    model3DVars = pattern3DModel.findall(content)
    spineVars = patternSpine.findall(content)
    # totalVars.extend(model3DVars) #3d模型在onHide的时候就需要dispose了
    totalVars.extend(spineVars)
    
    return totalVars,itemListVars,model3DVars,spineVars

def checkTargetFunNotExc(fileName,funcStr,fieldNames:list[str],funcName:str,excFunc:str="dispose")->bool:
    flag = False
    msgArr=[]
    i = 0
    while fieldNames and len(fieldNames)>0:
        varName = fieldNames[i].strip()
        # vn = varName+"."+excFunc+"()"
        # idx = funcStr.find(vn)
        # if idx <0:
        #     vn = varName+"?."+excFunc+"()"
        #     idx = funcStr.find(vn)
        if not checkVarFunExc(funcStr,varName,excFunc):
            msg = "{0}的[{1}]没有在{2}执行[{3}]".format(fileName,varName,funcName,excFunc)
            msgArr.append(msg)
            if not flag:
                flag = True
        else:
            #删掉已检查通过的
            fieldNames.pop(i)
            i-=1;
        i+=1
        if i>=len(fieldNames):
            break;
        
    return flag,msgArr

def checkVarFunExc(funcStr,varName,excFunc):
    funcNames = excFunc.split("#")
    if len(funcNames) >1: #多个函数名 只需执行一个即可
        flag = False
        for fn in funcNames:
            if fn !="" and fn !=None:
                flag = checkVarFunExc(funcStr,varName,fn);
                if flag:
                    break;
        return flag;
         
    vn = varName+"."+excFunc+"()"
    idx = funcStr.find(vn)
    if idx <0:
        vn = varName+"?."+excFunc+"()"
        idx = funcStr.find(vn);
    return idx >=0;
 
def readContent(svnFileOrDir):
    isFail = False
    try:
        changeFiles = []
        if os.path.isdir(svnFileOrDir) and os.path.exists(svnFileOrDir):
            logToFile("\n传入路径检查 Dir=",svnFileOrDir)
            changeFiles = CodeUtil.readDirFiles(svnFileOrDir)
        elif os.path.isfile(changeFile) and os.path.exists(changeFile):
            logToFile("\n通过SVN操作检测 svnFile=",svnFileOrDir)
            f = open(svnFileOrDir,"r")
            changeFiles = f.readlines();
            f.close()
        else:
            logToFile("\n","不合法的file参数=",svnFileOrDir)
            return SUCCESS

        # logToFile("changeFiles=",changeFiles)
        
        tsFiles = [f.strip() for f in changeFiles if os.path.splitext(f.strip())[1]==".ts"]
        # logToFile("tsFiles=",tsFiles)
        if tsFiles and len(tsFiles)>0:
            for f in tsFiles:
                if not os.path.exists(f): #更新列表中的文件可能是被删除的
                    continue;
                fail = checkIsNotStandard(f);
                if not isFail and fail:
                    isFail = fail;
    except Exception as e:
        logToFile("\n","readContent Exception e=",e)
        logToFile("\n","readContent Exception e.__traceback__=",e.__traceback__)

    if isFail:
        return FAIL
    return SUCCESS

def debugArgs(count):
    logToFile("\n*****调试信息开始***** 参数个数=",count)
    for i in range(count):
        curArg = sys.argv[i]
        logToFile("参数i=",i,"=",curArg)
        if i>1 and not curArg.startswith("-") and os.path.exists(curArg) and os.path.isfile(curArg):
            try:
                with open(curArg,"r",encoding="utf8") as f:
                    logToFile("\n文件内容=",f.read())
            except Exception as e:
                logToFile("debugArgs Exception e=",e)
    logToFile("*****调试信息结束*****")

def initLogFile(tms:time.struct_time):
    selfPath = sys.argv[0] #脚本所在目录
    rootTuple= os.path.split(selfPath)
    root = rootTuple[0]
    logRoot = os.path.join(root,"logs")
    if not os.path.exists(logRoot):
        os.mkdir(logRoot)
    fmt = "%Y-%m-%d_%H-%M-%S"; 
    logHead= LOG_FILE_HEAD
    tmLog = logHead+time.strftime(fmt,tms)+".txt"
    global LOG_FILE
    logPath = os.path.join(logRoot,tmLog)
    LOG_FILE = open(logPath,"w+",encoding="utf8")
    return logPath,logRoot

def removeOldLogFile(logRoot:str,logTime):

    logHead= LOG_FILE_HEAD

    checkLogFiles = os.listdir(logRoot)
    
    checkLogFiles = [i for i in checkLogFiles if os.path.split(i)[1].startswith(logHead)]
    fmt = "%Y-%m-%d_%H-%M-%S";
    now = time.time()
    if isinstance(logTime,str):
        try:
            logTime = int(logTime)
        except Exception as e:
            logTime = 0;
            logToFile("removeOldLogFile e=",e)
    if logTime > 0:
        for f in checkLogFiles:
            fPath = os.path.join(logRoot,f)
            if not os.path.exists(fPath):
                continue
            name = os.path.splitext(f)[0]
            nameStrArr = name.split("_")
            if len(nameStrArr)>=4:
                nameStrArr = nameStrArr[1:-1]
            else:
                nameStrArr = nameStrArr[1:]
            
            data = "_".join(nameStrArr)
            ftsm = time.strptime(data,fmt)
            sec=time.mktime(ftsm)
            difSec=now-sec
            if difSec>=logTime:#只保留一定时间内的
                os.remove(fPath)

if __name__=="__main__":
        
    tms = time.localtime()
    logPath,logRoot = initLogFile(tms)
    logTime = 0;
    try:
        argParser = argparse.ArgumentParser(description="代码规范检测工具");
        argParser.add_argument("-file",help="本次改变的文件列表");
        argParser.add_argument("-debug",help="是否开启参数调试:Y,开启;N,关闭",default="N");
        argParser.add_argument("-logTime",help="保留多久的日志文件单位秒,0则全部保留",default=0);
        
        #调试参数
        argParser.add_argument("-a",help="调试参数a");
        argParser.add_argument("-b",help="调试参数b");
        argParser.add_argument("-c",help="调试参数c");
        argParser.add_argument("-d",help="调试参数d");
        argParser.add_argument("-e",help="调试参数e");
        argInfo = argParser.parse_args()
        logTime = argInfo.logTime
        removeOldLogFile(logRoot,logTime)
    except Exception as e:
        logToFile("参数转换或删除日志文件错误 e=",e)

    count = len(sys.argv);
    timeStr = time.strftime("%Y-%m-%d %H:%M:%S",tms)
    ######本地测试 begin
    # st = time.perf_counter() 
    # fail = checkIsNotStandard("ts_src/CopyUIWorldBossExInfoView.ts");
    # print("本地调试结果 fail=",fail,"argInfo=",argInfo)
    # et = time.perf_counter()
    # logToFile(timeStr+"检测{}个文件耗时={:.2f}毫秒".format(TS_File_Num,(et-st)*1000))
    ######本地测试 end

    # IS_DEBUG = count>1 and sys.argv[1] =="Y"
    IS_DEBUG = count>1 and argInfo.debug =="Y"
    st = time.perf_counter()
    retCode = SUCCESS
    if argInfo.file !=None:
        changeFile = argInfo.file #sys.argv[2] #这个文件里面保存着全部修改的文件路径列表
        code = readContent(changeFile)
        if retCode!=code and code==FAIL:
            retCode = code
    sys.stdout.write(retCode)
    logToFile("retCode=",retCode,"IS_DEBUG=",IS_DEBUG)
    et = time.perf_counter()
    logToFile(timeStr+"检测{}个文件耗时={:.2f}毫秒,命中{}个疑似异常".format(TS_File_Num,(et-st)*1000,NOT_STANDER_COUNT))
    # 调试 begin
    if IS_DEBUG:
        debugArgs(count)
    # 调试 end
    LOG_FILE.close();
    try:
        tupleLog = os.path.split(logPath)
        dstName = os.path.splitext(tupleLog[1])[0]+"_"+retCode+".txt";
        dstPath = os.path.join(tupleLog[0],dstName)
        os.rename(logPath,dstPath);
    except Exception as e:
        logToFile("重命名日志文件出错=",e)
    